package com.datastructure2.dfynamicporgramming;

import jdk.internal.util.xml.impl.Input;
import org.junit.Test;
import sun.awt.image.PixelConverter;

import javax.lang.model.element.VariableElement;
import javax.management.remote.rmi._RMIConnection_Stub;
import java.sql.Array;
import java.util.*;
import java.util.logging.Level;

/**
 * @author: 临晖
 * @date: 2024/03/11
 * @description:
 */
public class Solution {

    /**
     * 509. 斐波那契数
     * 简单
     * 相关标签
     * 相关企业
     * 斐波那契数 （通常用 F(n) 表示）形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始，后面的每一项数字都是前面两项数字的和。也就是：
     * <p>
     * F(0) = 0，F(1) = 1
     * F(n) = F(n - 1) + F(n - 2)，其中 n > 1
     * 给定 n ，请计算 F(n) 。
     * <p>
     * <p>
     * 确定dp数组（dp table）以及下标的含义
     * 确定递推公式
     * dp数组如何初始化
     * 确定遍历顺序
     * 举例推导dp数组
     *
     * @param n
     * @return
     */
    public int fib(int n) {

        if (n == 0) {
            return 0;
        }

        if (n == 1) {
            return 1;
        }

        int[] dp = new int[n];      //dp[i]表示：前i项之和

        //确认递推公式 dp[i] = dp[i - 1] + dp[i - 2];

        //初始化dp数组
        dp[0] = 0;
        dp[1] = 1;


        //确认循环顺序
        for (int i = 2; i < dp.length; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }

        //返回值
        return dp[n - 1] + dp[n - 2];
    }


    /**
     * 70. 爬楼梯
     * 简单
     * 相关标签
     * 相关企业
     * 提示
     * 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
     * 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢？
     * <p>
     * <p>
     * * 确定dp数组（dp table）以及下标的含义
     * * 确定递推公式
     * * dp数组如何初始化
     * * 确定遍历顺序
     * * 举例推导dp数组
     *
     * @param n
     * @return
     */
    public int climbStairs(int n) {
        if (n == 0 || n == 1) {
            return 1;
        }


        //确定dp数组以及下标的含义，爬到第i阶楼梯，有dp[i]中方式
        int[] dp = new int[n + 1];
        //确认递推公式
        //dp[i] = dp[i - 1] + dp[i - 2]

        //初始化dp数组
        dp[0] = 1;
        dp[1] = 1;
        //确认遍历顺序
        for (int i = 2; i <= dp.length; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        //举例推到dp数组
        return dp[n];
    }


    /**
     * 746. 使用最小花费爬楼梯
     * 简单
     * 相关标签
     * 相关企业
     * 提示
     * 给你一个整数数组 cost ，其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用，即可选择向上爬一个或者两个台阶。
     * 你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
     * 请你计算并返回达到楼梯顶部的最低花费。
     * <p>
     * 确定递推公式
     * 确定dp数组（dp table）以及下标的含义
     * dp数组如何初始化
     * 确定遍历顺序
     * 举例推导dp数组
     *
     * @param cost
     * @return
     */
    public int minCostClimbingStairs(int[] cost) {

        //第一，确认dp数组及其含义 表示爬i阶楼梯花费的最少费用
        int[] dp = new int[cost.length + 1];
        //第二，确认递推公式
        //dp[i] = Math.min(cost[i - 1] + dp[i - 1], cost[i - 2] + dp[i - 2]);
        //第三，初始化dp数组
        //dp[2] = Math.min(cost[1], cost[0]);
        //dp[3] = Math.min(cost[2], cost[1]);
        //第四，确认遍历顺序
        for (int i = 2; i < dp.length; i++) {
            dp[i] = Math.min(cost[i - 1] + dp[i - 1], cost[i - 2] + dp[i - 2]);
        }

        return dp[dp.length - 1];

    }


    /**
     * 62. 不同路径
     * 中等
     * 相关标签
     * 相关企业
     * 一个机器人位于一个 m x n 网格的左上角 （起始点在下图中标记为 “Start” ）。
     * 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角（在下图中标记为 “Finish” ）。
     * 问总共有多少条不同的路径？
     * <p>
     * 动规五部曲：
     * 1.定义dp数组，明白其i及其dp[i]的含义
     * 2.确认递推公式
     * 3.初始化dp数组
     * 4.确认循环顺序
     * 5.举例推导
     *
     * @param m
     * @param n
     * @return
     */
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];     //dp[i][j]表示：到达(i + 1, j + 1)有dp[i][j]中方式


        //初始化
        for (int i = 0; i < dp[0].length; i++) {
            dp[0][i] = 1;
        }

        for (int i = 1; i < dp.length; i++) {
            dp[i][0] = 1;
        }

        //确认循环顺序
        for (int i = 1; i < dp.length; i++) {
            for (int j = 1; j < dp[i].length; j++) {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }

        return dp[m - 1][n - 1];


    }

    //一下是递归方式
    //public static int uniquePaths(int m, int n) {
    //    // 边界条件
    //    if (m == 1 || n == 1) {
    //        return 1;
    //    }
    //
    //    // 递归计算路径数
    //    int paths = uniquePaths(m-1, n) + uniquePaths(m, n-1);
    //
    //    return paths;
    //}

    //public void recursionUniquePath(int m, int n, int[][] dp, int count, int x, int y) {
    //    //确认递归退出方式
    //    if (x == m - 1 && y == n - 1) {
    //        count++;
    //        return;
    //    }
    //
    //    //递归逻辑
    //    dp[x][y] = 1;
    //
    //    if (x < m) {
    //        recursionUniquePath(m, n, dp, count, x + 1, y);
    //    } else if (y < n) {
    //        recursionUniquePath(m, n, dp, count, x, y + 1);
    //    }
    //
    //
    //
    //}


    /**
     * 63. 不同路径 II
     * 中等
     * 相关标签
     * 相关企业
     * 提示
     * 一个机器人位于一个 m x n 网格的左上角 （起始点在下图中标记为 “Start” ）。
     * 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角（在下图中标记为 “Finish”）。
     * 现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径
     * 网格中的障碍物和空位置分别用 1 和 0 来表示。
     * <p>
     * <p>
     * 动规五部曲
     *
     * @param obstacleGrid
     * @return
     */
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;

        if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) {//如果在起点或终点出现了障碍，直接返回0
            return 0;
        }

        int[][] dp = new int[m][n];

        //确认dp数组，及其含义
        for (int i = 0; i < n; i++) {
            if (obstacleGrid[0][i] == 1) {
                dp[0][i] = 0;
                break;
            }

            dp[0][i] = 1;
        }

        for (int i = 1; i < m; i++) {
            if (obstacleGrid[i][0] == 1) {
                dp[i][0] = 0;
                break;
            }

            dp[i][0] = 1;
        }


        //确认循环顺序
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                if (obstacleGrid[i][j] != 1) {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }

            }
        }

        return dp[m - 1][n - 1];
    }


    /**
     * 96. 不同的二叉搜索树
     * 中等
     * 相关标签
     * 相关企业
     * 给你一个整数 n ，求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种？返回满足题意的二叉搜索树的种数。
     *
     * @param n
     * @return
     */
    public int numTrees(int n) {
        if (n <= 1) {
            return 1;
        }


        int[] dp = new int[n + 1];

        dp[0] = 1;
        dp[1] = 1;


        for (int i = 2; i <= n; i++) {
            //dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]
            System.out.println("i   j-1   i-j   ");
            for (int j = 1; j <= i; j++) {
                dp[i] = dp[i] + (dp[j - 1] * dp[i - j]);

            }
        }

        return dp[n];

    }


    /**
     * 343. 整数拆分
     * 中等
     * 相关标签
     * 相关企业
     * 提示
     * 给定一个正整数 n ，将其拆分为 k 个 正整数 的和（ k >= 2 ），并使这些整数的乘积最大化。
     * 返回 你可以获得的最大乘积 。
     *
     * @param n
     * @return
     */
    public int integerBreak(int n) {

        //定义dp数组，明白dp[i]的含义；表示整数i被拆分后的最大乘积为dp[i]
        int[] dp = new int[n + 1];

        //初始化dp
        //dp[0] = 0;
        //dp[1] = 1;
        dp[2] = 1;


        //确认递推公式
        //dp[i] = dp[i / 2] * dp[i - i / 2];
        for (int i = 3; i <= n; i++) {
            for (int j = 2; j <= i; j++) {
                //拆分成俩个数字
                // j    i - j
                dp[i] = Math.max(dp[i], Math.max(i * (i - j), dp[i] * (i - j)));
            }
        }

        return dp[n];
    }


    //public static void main(String[] args) {
    //    // 背包容量 N
    //    // 物品种类 M
    //    Scanner sc = new Scanner(System.in);
    //
    //    int M = sc.nextInt();
    //    int N = sc.nextInt();
    //
    //    int[] values = new int[M];
    //    int[] weights = new int[M];
    //
    //    for(int i = 0; i < M;i++) {
    //        weights[i] = sc.nextInt();
    //    }
    //
    //
    //    for(int i = 0; i < M;i++) {
    //        values[i] = sc.nextInt();
    //    }
    //    testWeightBagProblem(weights,values,N);
    //
    //}

    /**
     * @param weight  物品重量
     * @param value   物品价值
     * @param bagSize 背包大小
     */
    //public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
    //    int[][] dp = new int[value.length + 1][bagSize + 1];        //第前i个物品在背包的价值是dp[i][j]
    //
    //    //初始化
    //    for (int i = 0; i < dp.length; i++) {
    //        dp[i][0] = 0;       //当背包大小为0的时候，价值只能为零
    //    }
    //
    //
    //
    //    for (int i = 1; i < dp.length; i++) {        //物品       i:物品    j:背包容量
    //        for (int j = 1; j < dp[i].length; j++) {
    //            if (j >= weight[i - 1]) {
    //                dp[i][j] = Math.max(dp[i - 1][j] , dp[i - 1][j - weight[i - 1]] + value[i - 1]);
    //            } else {
    //                dp[i][j] = dp[i - 1][j];
    //            }
    //        }
    //    }
    //
    //    // 打印dp数组
    //    System.out.println(dp[value.length][bagSize]);
    //}
    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize) {
        int[] dp = new int[bagSize + 1];        //第前i个物品在背包的价值是dp[i][j]

        dp[0] = 0;       //初始化


        for (int i = 0; i <= value.length; i++) {        //物品       i:物品    j:背包容量
            for (int j = bagSize; j >= 1; j--) {
                if (j >= weight[i]) {
                    //dp[i][j] = Math.max(dp[i - 1][j] , dp[i - 1][j - weight[i - 1]] + value[i - 1]);
                    dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
                } else {
                    //dp[i][j] = dp[i - 1][j];

                }
            }
        }

        // 打印dp数组
        System.out.println(dp[bagSize]);
    }


    /**
     * 416. 分割等和子集
     * 中等
     * 相关标签
     * 相关企业
     * 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集，使得两个子集的元素和相等。
     *
     * @param nums
     * @return
     */
    public boolean canPartition(int[] nums) {
        //定义dp数组

        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }

        if (sum % 2 != 0) {
            return false;
        }
        sum /= 2;       //背包大小

        //定义dp数组
        int[] dp = new int[sum + 1];        //i表示物品，j表示容量


        for (int i = 0; i < nums.length; i++) {
            for (int j = sum; j >= nums[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
            }

            if (dp[sum] == sum) {
                return true;
            }
        }


        return dp[sum] == sum;

    }


    /**
     * 1049. 最后一块石头的重量 II
     * 中等
     * 相关标签
     * 相关企业
     * 提示
     * 有一堆石头，用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
     * 每一回合，从中选出任意两块石头，然后将它们一起粉碎。假设石头的重量分别为 x 和 y，且 x <= y。那么粉碎的可能结果如下：
     * 如果 x == y，那么两块石头都会被完全粉碎；
     * 如果 x != y，那么重量为 x 的石头将会完全粉碎，而重量为 y 的石头新重量为 y-x。
     * 最后，最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下，就返回 0。
     *
     * @param stones
     * @return
     */
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for (int stone : stones) {
            sum += stone;
        }


        int target = sum / 2;          //11

        //定义dp数组
        int[] dp = new int[target + 1];

        for (int i = 0; i < stones.length; i++) {
            for (int j = target; j >= stones[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
            }
        }

        return target - dp[target];


    }


    /**
     * 494. 目标和
     * 中等
     * 相关标签
     * 相关企业
     * 给你一个非负整数数组 nums 和一个整数 target 。
     * 向数组中的每个整数前添加 '+' 或 '-' ，然后串联起所有整数，可以构造一个 表达式 ：
     * 例如，nums = [2, 1] ，可以在 2 之前添加 '+' ，在 1 之前添加 '-' ，然后串联起来得到表达式 "+2-1" 。
     * 返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
     *
     * @param nums
     * @param target
     * @return
     */
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }

        int x = (sum + target) / 2;

        if ((sum + target) % 2 != 0) {
            return 0;
        }

        if (Math.abs(target) > sum) {
            return 0;
        }
        //定义dp数组

        int[] dp = new int[x + 1];      //dp[i]表示：运算结果等于i得方式有dp[i]种

        //初始话
        dp[0] = 1;
        for (int i = 0; i < nums.length; i++) {
            for (int j = x; j >= nums[i]; j++) {
                dp[j] += dp[j - nums[i]];
            }
        }

        return dp[x];
    }


    /**
     * 474. 一和零
     * 中等
     * 相关标签
     * 相关企业
     * 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
     * 请你找出并返回 strs 的最大子集的长度，该子集中 最多 有 m 个 0 和 n 个 1 。
     * 如果 x 的所有元素也是 y 的元素，集合 x 是集合 y 的 子集 。
     *
     * @param strs
     * @param m
     * @param n
     * @return
     */
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp = new int[m + 1][n + 1]; //m个零，n个1，dp[i][j]  dp[i][j]最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。


        //初始化

        for (String str : strs) {
            int oneNum = 0;
            int zeroNum = 0;
            for (int num = 0; num < str.length(); num++) {

                if (str.charAt(num) == '1') {
                    oneNum++;
                } else if (str.charAt(num) == '0') {
                    zeroNum++;
                }

            }

            for (int i = m; i >= zeroNum; i--) {
                for (int j = n; j >= oneNum; j--) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
                }
            }

        }

        return dp[m][n];
    }


    /**
     * 518. 零钱兑换 II
     * 中等
     * 相关标签
     * 相关企业
     * 给你一个整数数组 coins 表示不同面额的硬币，另给一个整数 amount 表示总金额。
     * 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额，返回 0 。
     * 假设每一种面额的硬币有无限个。
     * 题目数据保证结果符合 32 位带符号整数。
     *
     * @param amount
     * @param coins
     * @return
     */
    public int change(int amount, int[] coins) {
        //定义dp数组
        int[] dp = new int[amount + 1];     //dp[j] 凑成金额为j的的硬币组合数有dp[j]种

        //初始化
        dp[0] = 1;

        //确认递推公式
        //dp[i] = Math.max(dp[i], dp[j - coins[i]] + 1);

        for (int i = 0; i < coins.length; i++) {            //i表示物品
            for (int j = coins[i]; j <= amount; j++) {          //j表示金额
                dp[j] += dp[j - coins[i]];
            }
        }

        return dp[amount];
    }


    /**
     * 377. 组合总和 Ⅳ
     * 中等
     * 相关标签
     * 相关企业
     * 给你一个由 不同 整数组成的数组 nums ，和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
     * 题目数据保证答案符合 32 位整数范围。
     *
     * @param nums
     * @param target
     * @return
     */
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target + 1];

        dp[0] = 1;

        //确认递推公式
        //dp[j] += dp[j - nums[i]];
        for (int i = 0; i <= target; i++) {
            for (int j = 0; j < nums.length; j++) {
                if (i >= nums[j]) {
                    dp[i] += dp[i - nums[j]];
                }
            }
        }

        return dp[target];
    }
    //遇到最大、最多，优先考虑Math.max();
    //遇到有多少种、多少方式，考虑 +=
    //如果有多少种，为组合时，不考虑顺序，那么就是先遍历物品
    //如果有多少种，为排列时，考虑顺序，那么就是先遍历背包


    //public static void main(String[] args) {
    //
    //    Scanner sc = new Scanner(System.in);
    //
    //    int n = sc.nextInt();
    //    int m = sc.nextInt();
    //    int[] dp = new int[n + 1];
    //    dp[0] = 1;
    //
    //    for (int i = 0; i <= n; i++) {      //背包容量
    //        for (int j = 1;j <= m; j++) {        //物品
    //            if (i >= j) {
    //                dp[i] += dp[i - j];
    //            }
    //        }
    //    }
    //
    //    System.out.println(dp[n]);
    //
    //
    //}


    /**
     * 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
     * 每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢？
     * 注意：给定 n 是一个正整数。
     *
     * @param n
     * @param m
     * @return
     */
    public int paLouTi(int n, int m) {         //有n阶楼梯，每次可爬1 - m阶楼梯
        int[] dp = new int[n + 1];
        dp[0] = 1;

        for (int i = 0; i <= n; i++) {      //背包容量
            for (int j = 1; j <= m; j++) {        //物品
                if (i >= j) {
                    dp[i] += dp[i - j];
                }
            }
        }

        return dp[n];

    }


    /**
     * 322. 零钱兑换
     * 中等
     * 相关标签
     * 相关企业
     * 给你一个整数数组 coins ，表示不同面额的硬币；以及一个整数 amount ，表示总金额。
     * 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额，返回 -1 。
     * 你可以认为每种硬币的数量是无限的。
     *
     * @param coins
     * @param amount
     * @return
     */
    public int coinChange(int[] coins, int amount) {
        long[] dp = new long[amount + 1];         //dp[i]表示：组成金额为i的最少硬币数有dp[i]个


        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;

        //for (int i = 0; i <= amount; i++) {
        //    for (int j = 0; j < coins.length; j++) {        //
        //        if (i >= coins[j]) {
        //
        //            //dp[j] = Math.max(dp[j], dp[j - coins[i]] + 1);
        //            dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
        //        }
        //
        //    }
        //}

        for (int i = 0; i < coins.length; i++) {
            for (int j = coins[i]; j <= amount; j++) {
                dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
            }
        }

        return (int) (dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount]);
    }


    /**
     * 279. 完全平方数
     * 已解答
     * 中等
     * 相关标签
     * 相关企业
     * 给你一个整数 n ，返回 和为 n 的完全平方数的最少数量 。
     * 完全平方数 是一个整数，其值等于另一个整数的平方；换句话说，其值等于一个整数自乘的积。例如，1、4、9 和 16 都是完全平方数，而 3 和 11 不是
     *
     * @param n
     * @return
     */
    public int numSquares(int n) {
        long[] dp = new long[n + 1];         //dp[i]表示：组成金额为i的最少硬币数有dp[i]个


        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;

        for (int i = 1; i <= (int) Math.ceil(Math.sqrt(n)); i++) {       //物品    ,1,2,3,4
            int weight = (int) Math.pow(i, 2);
            for (int j = weight; j <= n; j++) {          //背包
                dp[j] = Math.min(dp[j], dp[j - weight] + 1);
            }
        }

        return (int) dp[n];
    }


    /**
     * 139. 单词拆分
     * 中等
     * 相关标签
     * 相关企业
     * 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
     * 注意：不要求字典中出现的单词全部都使用，并且字典中的单词可以重复使用。
     *
     * @param s
     * @param wordDict
     * @return
     */
    public boolean wordBreak(String s, List<String> wordDict) {
        HashSet<String> set = new HashSet<>(wordDict);
        boolean[] dp = new boolean[s.length() + 1];     //背包

        //初始化
        dp[0] = true;

        for (int i = 1; i <= s.length(); i++) {     //背包
            for (int j = 0; j < i && !dp[i]; j++) {     //物品
                String subString = s.substring(j, i);

                if (set.contains(subString) && dp[j]) {
                    dp[i] = true;
                }
            }
        }

        return dp[s.length()];
    }


    /**
     * 198. 打家劫舍
     * 中等
     * 相关标签
     * 相关企业
     * 你是一个专业的小偷，计划偷窃沿街的房屋。每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警。
     * 给定一个代表每个房屋存放金额的非负整数数组，计算你 不触动警报装置的情况下 ，一夜之内能够偷窃到的最高金额
     *
     * @param nums
     * @return
     */
    //public int rob(int[] nums) {
    //    if (nums.length == 1) {
    //        return nums[0];
    //    } else if (nums.length == 2) {
    //        return Math.max(nums[0], nums[1]);
    //    }
    //    //定义dp数组
    //    int[] dp = new int[nums.length];
    //
    //    //初始化
    //    dp[0] = nums[0];
    //    dp[1] = Math.max(nums[0], nums[1]);
    //
    //
    //    //确认递推公式
    //    //dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
    //    for (int i = 2; i < nums.length; i++) {
    //        dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
    //    }
    //
    //    return dp[nums.length - 1];
    //}


    /**
     *
     * 213. 打家劫舍 II
     * 中等
     * 相关标签
     * 相关企业
     * 提示
     * 你是一个专业的小偷，计划偷窃沿街的房屋，每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ，这意味着第一个房屋和最后一个房屋是紧挨着的。
     * 同时，相邻的房屋装有相互连通的防盗系统，如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警 。
     * 给定一个代表每个房屋存放金额的非负整数数组，计算你 在不触动警报装置的情况下 ，今晚能够偷窃到的最高金额
     *
     * @param nums
     * @return
     */
    //public int rob(int[] nums) {
    //    if (nums.length == 1) {
    //        return nums[0];
    //    }
    //
    //    int result1 = robRange(Arrays.copyOfRange(nums, 0, nums.length - 1));
    //    int result2 = robRange(Arrays.copyOfRange(nums, 1, nums.length));
    //
    //    return Math.max(result1, result2);
    //}
    //
    //public int robRange(int [] nums) {
    //    if (nums.length == 1) {
    //        return nums[0];
    //    }
    //
    //    int[] dp = new int[nums.length];
    //
    //    dp[0] = nums[0];
    //    dp[1] = Math.max(nums[0], nums[1]);
    //
    //    for (int i = 2; i < nums.length; i++) {
    //        dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
    //    }
    //
    //    return dp[nums.length - 1];
    //
    //
    //}


    /**
     * 337. 打家劫舍 III
     * 中等
     * 相关标签
     * 相关企业
     * 小偷又发现了一个新的可行窃的地区。这个地区只有一个入口，我们称之为 root 。
     * <p>
     * 除了 root 之外，每栋房子有且只有一个“父“房子与之相连。一番侦察之后，聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ，房屋将自动报警。
     * <p>
     * 给定二叉树的 root 。返回 在不触动警报的情况下 ，小偷能够盗取的最高金额 。
     *
     * @param root
     * @return
     */
    public int rob(TreeNode root) {
        int[] dp = robTree(root);
        return Math.max(dp[0], dp[1]);
    }


    //采用后续遍历
    public int[] robTree(TreeNode node) {
        if (node == null) {
            return new int[]{0, 0};     //dp[0]:表示不偷 dp[1]:表示偷
        }

        int[] dp1 = robTree(node.left);
        int[] dp2 = robTree(node.right);


        //偷当前节点
        int val1 = dp1[0] + dp2[0] + node.val;

        //不偷当前节点
        int val2 = Math.max(dp1[0], dp2[0]) + Math.max(dp1[1], dp2[1]);

        //return dp;

        return new int[]{val2, val1};

    }


    /**
     *
     * 121. 买卖股票的最佳时机
     * 简单
     * 相关标签
     * 相关企业
     * 给定一个数组 prices ，它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
     * 你只能选择 某一天 买入这只股票，并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
     * 返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回 0 。
     *
     * @param prices
     * @return
     */
    //public int maxProfit(int[] prices) {
    //    ////持有， 不持有
    //    //int[][] dp = new int[prices.length][2];     //0表示持有，1表示不持有
    //    //
    //    ////初始化dp数组
    //    //dp[0][0] = -prices[0];
    //    //dp[0][1] = 0;
    //    //
    //    //
    //    //
    //    //for (int i = 1; i < prices.length; i++) {
    //    //    dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
    //    //    dp[i][1] = Math.max(prices[i] + dp[i - 1][0], dp[i - 1][1]);
    //    //}
    //    //
    //    //return dp[prices.length - 1][1];
    //
    //    int low = Integer.MAX_VALUE;
    //    int result = 0;
    //
    //    for (int i = 0; i < prices.length - 1; i++) {
    //        low = Math.min(prices[i], low);
    //        result = Math.max(result, prices[i + 1] - low);
    //    }
    //
    //    return result;
    //}


    /**
     * 122. 买卖股票的最佳时机 II
     * 已解答
     * 中等
     * 相关标签
     * 相关企业
     * 给你一个整数数组 prices ，其中 prices[i] 表示某支股票第 i 天的价格。
     * 在每一天，你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买，然后在 同一天 出售。
     * 返回 你能获得的 最大 利润 。
     *
     * @param prices
     * @return
     */
    //public int maxProfit(int[] prices) {
    //    //int sum = 0;
    //    //
    //    //for (int i = 1; i < prices.length; i++) {
    //    //    if (prices[i] - prices[ i - 1] > 0) {
    //    //        sum += (prices[i] - prices[i - 1]);
    //    //    }
    //    //}
    //    //
    //    //return sum;
    //
    //    //持有，不持有
    //    int[][] dp = new int[prices.length][2];
    //    dp[0][0] = -prices[0];
    //    dp[0][1] = 0;
    //
    //    for (int i = 1; i < dp.length; i++) {
    //        dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
    //        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
    //        //dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
    //        //dp[i][1] = Math.max(prices[i] + dp[i - 1][0], dp[i - 1][1]);
    //
    //    }
    //
    //    return dp[prices.length - 1][1];
    //}


    /**
     * 123. 买卖股票的最佳时机 III
     * 困难
     * 相关标签
     * 相关企业
     * 给定一个数组，它的第 i 个元素是一支给定的股票在第 i 天的价格。
     * 设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
     * 注意：你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）
     *
     * @param prices
     * @return
     */
    //public int maxProfit(int[] prices) {
    //    int[][] dp = new int[prices.length][5];
    //
    //    //无状态，第一次持有，第一次售卖，第二次持有，第二次售卖       0, 1, 2, 3, 4
    //
    //    dp[0][1] = -prices[0];
    //    dp[0][3] = -prices[0];
    //
    //    for (int i = 1; i < prices.length; i++) {
    //        dp[i][0] = dp[i - 1][0];
    //
    //        dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);      //连续持有，购买
    //        dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);              //无状态转移，售卖
    //        dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);        //连续持有，重新买入
    //        dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);        //
    //    }
    //
    //    return dp[prices.length - 1][4];
    //}


    /**
     * 188. 买卖股票的最佳时机 IV
     * 困难
     * 相关标签
     * 相关企业
     * 给你一个整数数组 prices 和一个整数 k ，其中 prices[i] 是某支给定的股票在第 i 天的价格。
     * 设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说，你最多可以买 k 次，卖 k 次。
     * 注意：你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。
     *
     * @param k
     * @param prices
     * @return
     */
    //public int maxProfit(int k, int[] prices) {
    //
    //    //定义dp数组
    //    int[][] dp = new int[prices.length][2 * k + 1];
    //
    //    //初始化 默认 （持有， 不持有) * n
    //    for (int i = 1; i <= 2 * k; i += 2) {
    //        dp[0][i] = -prices[0];
    //    }
    //
    //    //确认递推公式
    //
    //    //确认循环顺序
    //    for (int i = 1; i < prices.length; i++) {
    //        dp[i][0] = dp[i - 1][0];
    //        for (int j = 1; j <= 2 * k; j += 2) {           //1 3 5 7
    //            //dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
    //            //dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
    //            //dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
    //            //dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
    //
    //            dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
    //            dp[i][j + 1] = Math.max(dp[i - 1][j + 1], dp[ i - 1][j] + prices[i]);
    //        }
    //    }
    //
    //    return dp[prices.length - 1][2 * k];
    //
    //}


    /**
     * 309. 买卖股票的最佳时机含冷冻期
     * 中等
     * 相关标签
     * 相关企业
     * 给定一个整数数组prices，其中第  prices[i] 表示第 i 天的股票价格 。​
     * 设计一个算法计算出最大利润。在满足以下约束条件下，你可以尽可能地完成更多的交易（多次买卖一支股票）:
     * 卖出股票后，你无法在第二天买入股票 (即冷冻期为 1 天)。
     * 注意：你不能同时参与多笔交易（你必须在再次购买前出售掉之前的股票）。
     *
     * @param prices
     * @return
     */
    public int maxProfit(int[] prices) {
        //买入 保持卖出  今天卖出 冷冻期
        int[][] dp = new int[prices.length][4];

        //初始化
        dp[0][0] = -prices[0];

        for (int i = 1; i < prices.length; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], Math.max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]));
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][3]);
            dp[i][2] = dp[i - 1][0] + prices[i];
            dp[i][3] = dp[i - 1][2];
        }

        return Math.max(dp[prices.length - 1][3], Math.max(dp[prices.length - 1][1], dp[prices.length - 1][2]));
    }


    /**
     * 714. 买卖股票的最佳时机含手续费
     * 中等
     * 相关标签
     * 相关企业
     * 提示
     * 给定一个整数数组 prices，其中 prices[i]表示第 i 天的股票价格 ；整数 fee 代表了交易股票的手续费用。
     * 你可以无限次地完成交易，但是你每笔交易都需要付手续费。如果你已经购买了一个股票，在卖出它之前你就不能再继续购买股票了。
     * 返回获得利润的最大值。
     * 注意：这里的一笔交易指买入持有并卖出股票的整个过程，每笔交易你只需要为支付一次手续费
     *
     * @param prices
     * @param fee
     * @return
     */
    public int maxProfit(int[] prices, int fee) {
        //定义dp      购买，售出
        int[][] dp = new int[prices.length][2];

        dp[0][0] = -prices[0];

        for (int i = 1; i < prices.length; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee);
        }

        return dp[prices.length - 1][1];
    }


    /**
     * 300. 最长递增子序列
     * 中等
     * 相关标签
     * 相关企业
     * 给你一个整数数组 nums ，找到其中最长严格递增子序列的长度。
     * 子序列 是由数组派生而来的序列，删除（或不删除）数组中的元素而不改变其余元素的顺序。例如，[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的
     * 子序列
     *
     * @param nums
     * @return
     */
    public int lengthOfLIS(int[] nums) {

        if (nums.length <= 1) {
            return nums.length;
        }
        //定义dp数组，dp[i]的含义：在i之前包括i结尾的nums[i]的最长子序列长度为dp[i]（切记，以nums[i]结尾，意思就是，子序列必须要包括nums[i]）
        int[] dp = new int[nums.length];

        //初始化
        Arrays.fill(dp, 1);

        //定义递推公式


        //10,9,2,5,3,7,101,18
        int res = 0;
        for (int i = 1; i < nums.length; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }

            if (dp[i] > res) {
                res = dp[i];
            }
        }


        return res;
    }


    /**
     * 674. 最长连续递增序列
     * 简单
     * 相关标签
     * 相关企业
     * 给定一个未经排序的整数数组，找到最长且 连续递增的子序列，并返回该序列的长度。
     * <p>
     * 连续递增的子序列 可以由两个下标 l 和 r（l < r）确定，如果对于每个 l <= i < r，都有 nums[i] < nums[i + 1] ，那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。
     *
     * @param nums
     * @return
     */
    public int findLengthOfLCIS(int[] nums) {
        if (nums.length <= 1) {
            return nums.length;
        }
        //
        //int res = 0;
        //int max = 1;
        //for (int i = 1; i < nums.length; i++) {
        //    if (nums[i] > nums[i - 1]) {
        //        max++;
        //    } else {
        //        max = 1;
        //    }
        //
        //    if (max > res) {
        //        res = max;
        //    }
        //}
        //
        //return res;

        //定义dp数组，dp[i]表示，以dp[i]结尾的连续递增最长子序列为dp[i]
        int[] dp = new int[nums.length];

        Arrays.fill(dp, 1);

        int res = 0;
        //定义递推公式
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > nums[i - 1]) {
                dp[i] = Math.max(dp[i], dp[i - 1] + 1);
            }

            if (dp[i] > res) {
                res = dp[i];
            }
        }

        return res;
    }


    /**
     * 718. 最长重复子数组
     * 中等
     * 相关标签
     * 相关企业
     * 提示
     * 给两个整数数组 nums1 和 nums2 ，返回 两个数组中 公共的 、长度最长的子数组的长度 。
     *
     * @param nums1
     * @param nums2
     * @return
     */
    public int findLength(int[] nums1, int[] nums2) {

        //dp[i][j]表示：以num1[i]结尾的子序列和以nums2[j]结尾的子序列最长的重复子数组为dp[i][j]
        int[][] dp = new int[nums1.length][nums2.length];

        int res = 0;
        //初始化
        for (int i = 0; i < nums2.length; i++) {
            if (nums1[0] == nums2[i]) {
                dp[0][i] = 1;
                res = 1;
            }
        }

        for (int i = 1; i < nums1.length; i++) {
            if (nums2[0] == nums1[i]) {
                dp[i][0] = 1;
                res = 1;
            }
        }


        for (int i = 1; i < nums1.length; i++) {
            for (int j = 1; j < nums2.length; j++) {
                if (nums1[i] == nums2[j]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }

                if (dp[i][j] > res) {
                    res = dp[i][j];
                }
            }


        }
        return res;

    }


    /**
     * 1143. 最长公共子序列
     * 中等
     * 相关标签
     * 相关企业
     * 提示
     * 给定两个字符串 text1 和 text2，返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ，返回 0 。
     * <p>
     * 一个字符串的 子序列 是指这样一个新的字符串：它是由原字符串在不改变字符的相对顺序的情况下删除某些字符（也可以不删除任何字符）后组成的新字符串。
     * <p>
     * 例如，"ace" 是 "abcde" 的子序列，但 "aec" 不是 "abcde" 的子序列。
     * 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
     *
     * @param text1
     * @param text2
     * @return
     */
    public int longestCommonSubsequence(String text1, String text2) {
        //dp[i][j]：长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j] ，可以不包含text1.charAt(i)和text2.charAt(j)
        int[][] dp = new int[text1.length()][text2.length()];

        boolean flag = false;
        for (int i = 0; i < text1.length(); i++) {
            if (text1.charAt(i) == text2.charAt(0) || flag) {
                dp[i][0] = 1;
                flag = true;
            }
        }

        flag = false;
        for (int i = 0; i < text2.length(); i++) {
            if (text2.charAt(i) == text1.charAt(0) || flag) {
                dp[0][i] = 1;
                flag = true;
            }
        }


        for (int i = 1; i < text1.length(); i++) {
            for (int j = 1; j < text2.length(); j++) {
                if (text1.charAt(i) == text2.charAt(j)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
                }
            }
        }

        return dp[text1.length() - 1][text2.length() - 1];
    }


    /**
     * 1035. 不相交的线
     * 中等
     * 相关标签
     * 相关企业
     * 提示
     * 在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
     * 现在，可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线，这些直线需要同时满足：
     * nums1[i] == nums2[j]
     * 且绘制的直线不与任何其他连线（非水平线）相交。
     * 请注意，连线即使在端点也不能相交：每个数字只能属于一条连线。
     * 以这种方法绘制线条，并返回可以绘制的最大连线数。
     *
     * @param nums1
     * @param nums2
     * @return
     */
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        //定义dp数组
        int[][] dp = new int[nums1.length][nums2.length];

        boolean flag = false;
        //初始化
        for (int i = 0; i < nums1.length; i++) {
            if (nums2[0] == nums1[i] || flag) {
                dp[i][0] = 1;
                flag = true;
            }
        }
        flag = false;
        for (int i = 0; i < nums2.length; i++) {
            if (nums1[0] == nums2[i] || flag) {
                dp[0][i] = 1;
                flag = true;
            }
        }
        //确认递推公式

        //确认循环顺序

        for (int i = 1; i < nums1.length; i++) {
            for (int j = 1; j < nums2.length; j++) {
                if (nums1[i] == nums2[j]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        return dp[nums1.length - 1][nums2.length - 1];
    }


    /**
     * 53. 最大子数组和
     * 已解答
     * 中等
     * 相关标签
     * 相关企业
     * 给你一个整数数组 nums ，请你找出一个具有最大和的连续子数组（子数组最少包含一个元素），返回其最大和。
     *
     * @param nums
     * @return
     */
    public int maxSubArray(int[] nums) {

        //方式一
        //int sum = 0;
        //int res = Integer.MIN_VALUE;
        //for (int i = 0; i < nums.length; i++) {
        //    sum += nums[i];
        //
        //    if (sum > res) {
        //        res = sum;
        //    }
        //
        //    if (sum < 0) {
        //        sum = 0;
        //    }
        //
        //}
        //
        //return res;

        //方式二


        //dp[i]表示，以nums[i]为结尾的序列的子序列最大和为dp[i]
        int[] dp = new int[nums.length];

        //初始化
        dp[0] = nums[0];


        //确认递推公式
        //dp[i] = Math.max(dp[i - 1] + nums[i], dp[i - 1]);

        int res = dp[0];
        for (int i = 1; i < nums.length; i++) {
            dp[i] = Math.max(nums[i], dp[i - 1] + nums[i]);

            if (dp[i] > res) {
                res = dp[i];
            }
        }

        return res;

    }


    /**
     * 392. 判断子序列
     * 简单
     * 相关标签
     * 相关企业
     * 给定字符串 s 和 t ，判断 s 是否为 t 的子序列。
     * 字符串的一个子序列是原始字符串删除一些（也可以不删除）字符而不改变剩余字符相对位置形成的新字符串。（例如，"ace"是"abcde"的一个子序列，而"aec"不是）。
     *
     * @param s
     * @param t
     * @return
     */
    public boolean isSubsequence(String s, String t) {
        if ("".equals(s)) {
            return true;
        }

        if (s.length() > t.length()) {
            return false;
        }

        if (s.length() == t.length() && s.equals(t)) {
            return true;
        }
        String text1 = s;
        String text2 = t;
        //dp[i][j]：长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j] ，可以不包含text1.charAt(i)和text2.charAt(j)
        int[][] dp = new int[text1.length()][text2.length()];

        boolean flag = false;
        for (int i = 0; i < text1.length(); i++) {
            if (text1.charAt(i) == text2.charAt(0) || flag) {
                dp[i][0] = 1;
                flag = true;
            }
        }

        flag = false;
        for (int i = 0; i < text2.length(); i++) {
            if (text2.charAt(i) == text1.charAt(0) || flag) {
                dp[0][i] = 1;
                flag = true;
            }
        }


        for (int i = 1; i < text1.length(); i++) {
            for (int j = 1; j < text2.length(); j++) {
                if (text1.charAt(i) == text2.charAt(j)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
                }
            }
        }

        return dp[text1.length() - 1][text2.length() - 1] == text1.length();

    }


    /**
     * 115. 不同的子序列
     * 困难
     * 相关标签
     * 相关企业
     * 给你两个字符串 s 和 t ，统计并返回在 s 的 子序列 中 t 出现的个数，结果需要对 109 + 7 取模。
     *
     * @param s
     * @param t
     * @return
     */
    public int numDistinct(String s, String t) {
        //定义dp数组
        int[][] dp = new int[s.length() + 1][t.length() + 1];

        //初始化
        for (int i = 0; i < s.length() + 1; i++) {
            dp[i][0] = 1;
        }
        //确认递推公式

        //确认循环数组
        for (int i = 1; i <= s.length(); i++) {
            for (int j = 1; j <= t.length(); j++) {
                if (s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }

        return dp[s.length()][t.length()];
    }


    /**
     * 583. 两个字符串的删除操作
     * 中等
     * 相关标签
     * 相关企业
     * 给定两个单词 word1 和 word2 ，返回使得 word1 和  word2 相同所需的最小步数。
     * 每步 可以删除任意一个字符串中的一个字符
     *
     * @param word1
     * @param word2
     * @return
     */
    //public int minDistance(String word1, String word2) {
    //    //int[][] dp = new int[word1.length() + 1][word2.length() + 1];
    //    //
    //    //
    //    //for (int i = 0; i < word2.length() + 1; i++) {
    //    //    dp[0][i] = i;
    //    //}
    //    //
    //    //for (int i = 1; i < word1.length() + 1; i++) {
    //    //    dp[i][0] = i;
    //    //}
    //    //
    //    //for (int i = 1; i <= word1.length(); i++) {
    //    //    for (int j = 1; j <= word2.length(); j++) {
    //    //        if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
    //    //            dp[i][j] = dp[i - 1][j - 1];
    //    //        } else {
    //    //            dp[i][j] = Math.min(dp[i - 1][j - 1] + 2,
    //    //                    Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1));
    //    //        }
    //    //    }
    //    //}
    //    //
    //    //return dp[word1.length()][word2.length()];
    //
    //
    //    //确认dp数组含义  //dp[i][j]表示，word1与word2相同的字符有dp[i][j]个
    //    int[][] dp = new int[word1.length() + 1][word2.length() + 1];
    //
    //    dp[0][0] = 0;
    //    //int res = 0;
    //    for (int i = 1; i < word1.length() + 1; i++) {
    //        for (int j = 1; j < word2.length() + 1; j++) {
    //            if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
    //                dp[i][j] = dp[i - 1][j - 1] + 1;
    //            } else {
    //                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
    //            }
    //
    //
    //        }
    //    }
    //
    //    return word1.length() + word2.length() - 2 * dp[word1.length()][word2.length()];
    //}


    /**
     * 72. 编辑距离
     * 中等
     * 相关标签
     * 相关企业
     * 给你两个单词 word1 和 word2， 请返回将 word1 转换成 word2 所使用的最少操作数  。
     * <p>
     * 你可以对一个单词进行如下三种操作：
     * <p>
     * 插入一个字符
     * 删除一个字符
     * 替换一个字符
     *
     * @param word1
     * @param word2
     * @return
     */
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length() + 1][word2.length() + 1];

        for (int i = 0; i <= word1.length(); i++) {
            dp[i][0] = i;
        }

        for (int i = 0; i <= word2.length(); i++) {
            dp[0][i] = i;
        }


        for (int i = 1; i <= word1.length(); i++) {
            for (int j = 1; j <= word2.length(); j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    //增、删
                    dp[i][j] = Math.min(dp[i - 1][j - 1],
                            Math.min(dp[i - 1][j], dp[i][j - 1])) + 1;
                }
            }
        }

        return dp[word1.length()][word2.length()];
    }


    /**
     * 647. 回文子串
     * 中等
     * 相关标签
     * 相关企业
     * 提示
     * 给你一个字符串 s ，请你统计并返回这个字符串中 回文子串 的数目。
     * 回文字符串 是正着读和倒过来读一样的字符串。
     * 子字符串 是字符串中的由连续字符组成的一个序列。
     * 具有不同开始位置或结束位置的子串，即使是由相同的字符组成，也会被视作不同的子串。
     *
     * @param s
     * @return
     */
    public int countSubstrings(String s) {
        boolean[][] dp = new boolean[s.length()][s.length()];

        int res = 0;

        for (int i = s.length() - 1; i >= 0; i--) {
            for (int j = i; j < s.length(); j++) {
                if (s.charAt(i) == s.charAt(j)) {
                    if (Math.abs(j - i) <= 1) {
                        res++;
                        dp[i][j] = true;
                    } else if (dp[i + 1][j - 1]) {
                        res++;
                        dp[i][j] = true;
                    }
                }
            }
        }

        return res;
    }


    /**
     * 给你一个字符串 s ，找出其中最长的回文子序列，并返回该序列的长度。
     * <p>
     * 子序列定义为：不改变剩余字符顺序的情况下，删除某些字符或者不删除任何字符形成的一个序列。
     *
     * @param s
     * @return
     */
    public int longestPalindromeSubseq(String s) {
        int len = s.length();
        int[][] dp = new int[len + 1][len + 1];
        for (int i = len - 1; i >= 0; i--) { // 从后往前遍历 保证情况不漏
            dp[i][i] = 1; // 初始化
            for (int j = i + 1; j < len; j++) {
                if (s.charAt(i) == s.charAt(j)) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    dp[i][j] = Math.max(dp[i + 1][j], Math.max(dp[i][j], dp[i][j - 1]));
                }
            }
        }
        return dp[0][len - 1];
    }

    @Test
    public void myTest() {
        String s = "aaa";
        boolean[][] dp = new boolean[s.length()][s.length()];

        int res = 0;

        for (int i = s.length() - 1; i >= 0; i--) {
            for (int j = i; j < s.length(); j++) {
                System.out.print(j + " " + i + "   /   ");
            }
            System.out.println();
        }



    }
}
